No cabe duda de que Python es un lenguaje flexible, y cuando trabajamos con funciones no es una excepción.
En Python, dentro de una función podemos definir otras funciones. Con la peculiaridad de que el ámbito de estas funciones se encuentre únicamente dentro de la función padre. Vamos a trabajar los ámbitos un poco más en profundidad:
In [1]:
def hola():
def bienvenido():
return "Hola!"
return bienvenido
Si intentamos llamar a la función bienvenido...
In [3]:
bienvenido()
Como vemos nos da un error de que no existe. En cambio si intentamos ejecutar la función hola():
In [2]:
hola()
Out[2]:
Se devuelve la función bienvenido, y podemos apreciar dentro de su definición que existe un espacio llamado locals, el cual hace referencia al ámbito local que abarca la función.
Si utilizamos una función reservada locals() obtendremos un diccionario con todas las definiciones dentro del espacio local del bloque en el que estamos:
In [2]:
def hola():
def bienvenido():
return "Hola!"
print( locals() ) # Mostramos el ámbito local
hola()
Como vemos se nos muestra un diccionario, aquí encontraremos la función bienvenido().
Podríamos añadir algo más:
In [5]:
lista = [1,2,3]
def hola():
numero = 50
def bienvenido():
return "Hola!"
print( locals() ) # Mostramos el ámbito local
hola()
Como podemos observar, ahora además de la función tenemos una clave con el número y el valor 50. Sin embargo no encontramos la lista, pues esta se encuentra fuera del ámbito local. De hecho se encuentra en el ámbito global, el cual podemos mostrar con la función reservada globals():
In [1]:
# Antes de ejecutar este bloque reinicia el Notebook para vaciar la memoria.
lista = [1,2,3]
def hola():
numero = 50
def bienvenido():
return "Hola!"
print( globals() ) # Mostramos el ámbito global
hola()
Tampoco es necesario que nos paremos a analizar el contenido, pero como podemos observar, desde el ámbito global tenemos acceso a muchas más definiciones porque engloba a su vez todas las de sus bloques padres.
Si mostramos únicamente las claves del diccionario globals(), quizá sería más entendible:
In [5]:
globals().keys()
Out[5]:
Ahora si buscamos bien encontraremos la clave lista, la cual hace referencia a la variable declarada fuera de la función. Incluso podríamos acceder a ella como si fuera un diccionario normal:
In [9]:
globals()['lista'] # Desde la función globals
In [12]:
lista # Forma tradicional
Out[12]:
Volviendo a nuestra función hola(), ahora sabemos que si la ejecutamos, en realidad estamos accediendo a su función local bienvenido(), pero eso no significa que la ejecutamos, sólo estamos haciendo referencia a ella.
Esa es la razón de que se devuelva su definición y no el resultado de su ejecución:
In [13]:
def hola():
def bienvenido():
return "Hola!"
return bienvenido
hola()
Out[13]:
Por muy raro que parezca, podríamos ejecutarla llamando una segunda vez al paréntesis. La primera para hola() y la segunda para bienvenido():
In [15]:
hola()()
Out[15]:
Como es realmente extraño, normalmente lo que hacemos es asignar la función a una variable y ejecutarla como si fuera una nueva función:
In [16]:
bienvenido = hola()
bienvenido()
Out[16]:
A diferencia de las colecciones y los objetos, donde las copias se utilizaban como accesos directos, las copias de las funciones son independientes y aunque borrásemos la original, la nueva copia seguiría existiendo:
In [17]:
del(hola)
bienvenido()
Out[17]:
In [23]:
def hola():
return "Hola!"
def test(funcion):
print( funcion() )
test(hola)
Quizá en este momento no se ocurren muchas utilidades para esta funcionalidad, pero creedme que es realmente útil cuando queremos extender funciones ya existentes sin modificarlas. De ahí que este proceso se conozca como un decorador, y de ahí pasamos directamente a las funciones decoradoras.
In [7]:
def hola():
print("Hola!")
def adios():
print("Adiós!")
Y queremos queremos crear un decorador para monitorizar cuando se ejecutan las dos funciones, avisando antes y después.
Para crear una función decoradora tenemos que recibir la función a ejecutar, y envolver su ejecución con el código a extender:
In [8]:
def monitorizar(funcion):
def decorar():
print("\t* Se está apunto de ejecutar la función:", funcion.__name__)
funcion()
print("\t* Se ha finalizado de ejecutar la función:", funcion.__name__)
return decorar
Ahora para realizar la monitorización deberíamos llamar al monitor ejecutando la función enviada y devuelta:
In [9]:
monitorizar(hola)()
Sin embargo esto no es muy cómodo, y ahí es cuando aparece la sintaxis que nos permite configurar una función decoradora en una función normal:
In [10]:
@monitorizar
def hola():
print("Hola!")
@monitorizar
def adios():
print("Adiós!")
Una vez configurada la función decoradora, al utilizar las funciones se ejecutarán automáticamente dentro de la función decoradora:
In [11]:
hola()
print()
adios()
In [12]:
def monitorizar_args(funcion):
def decorar(*args,**kwargs):
print("\t* Se está apunto de ejecutar la función:", funcion.__name__)
funcion(*args,**kwargs)
print("\t* Se ha finalizado de ejecutar la función:", funcion.__name__)
return decorar
@monitorizar_args
def hola(nombre):
print("Hola {}!".format(nombre))
@monitorizar_args
def adios(nombre):
print("Adiós {}!".format(nombre))
hola("Héctor")
print()
adios("Héctor")
Perfecto! Ahora ya sabes qué son las funciones decoradoras y cómo utilizar el símbolo @ para automatizar su ejecución. Estas funciones se utilizan mucho cuando trabajamos con Frameworks Web como Django, así que seguro te harán servicio si tienes pensado aprender a utilizarlo.